Este material consiste na resolução das atividades da disciplina Processamento Digital de Imagens lecionada por Agostinho Brito Jr. Todas as atividades constam em http://agostinhobritojr.github.io/tutoriais/pdi/. Além disso, a resolução e códigos de todas essas atividades se encontram no meu GitHub: https://github.com/vanessadants. Para voltar para o início acessar https://vanessadants.github.io/
1. Unidade 1
1.1. Manipulando pixels em uma imagem
1.2. Negativo em retângulo
-
Utilizando o programa exemplos/pixels.cpp como referência, implemente um programa regions.cpp. Esse programa deverá solicitar ao usuário as coordenadas de dois pontos P1 e P2 localizados dentro dos limites do tamanho da imagem e exibir que lhe for fornecida. Entretanto, a região definida pelo retângulo de vértices opostos definidos pelos pontos P1 e P2 será exibida com o negativo da imagem na região correspondente. O efeito é ilustrado na Figura Regiões.
Para resolver essa questão implementamos o código abaixo:
#include <iostream>
#include <opencv2/opencv.hpp>
using namespace cv;
using namespace std;
int main(int, char**){
Mat image;//matriz de pixels da imagem
int altura, largura;// altura e largura da imagem
Vec2i P1, P2; //pontos que o usuário vai informar e servirão para delimitar o retângulo
image= imread("vanessa.png",CV_LOAD_IMAGE_GRAYSCALE); //carregar a imagem 780 x 1028
if(!image.data)
cout << "nao abriu vanessa.png" << endl;
namedWindow("janela",WINDOW_AUTOSIZE);
altura=image.rows;
largura=image.cols;
//pedir os pontos ao usuário (ponto Pi(x,y))
do{
cout<<"Insira as coordenadas do ponto P1 (x,y):\n";
cin>>P1[0]>>P1[1];
}while(P1[0]<0 || P1[0]>=altura || P1[1]<0 || P1[1]>=largura);
do{
cout<<"Insira as coordenadas do ponto P2 (x,y):\n";
cin>>P2[0]>>P2[1];
}while(P2[0]<0 || P2[0]>=altura || P2[1]<0 || P2[1]>=largura);
//exibir a imagem antes de qualquer modificação
imshow("janela", image);
waitKey();
//Fazer a região definida por esses pontos ficar em negativo
int li,lf,ci,cf; //servirá para limitar o retângulo
li=(P1[0]<=P2[0]?P1[0]:P2[0]);
lf=(P1[0]>P2[0]?P1[0]:P2[0]);
ci=(P1[1]<=P2[1]?P1[1]:P2[1]);
cf=(P1[1]>P2[1]?P1[1]:P2[1]);
for(int i=li;i<lf;i++){
for(int j=ci;j<cf;j++){
image.at<uchar>(i,j)=255-image.at<uchar>(i,j); //negativo da imagem
}
}
//imagem com a região retangular definida por P1 e P2 negativa
imshow("janela", image);
waitKey();
return 0;
}
Conforme podemos perceber o código recebe 2 pontos do usuário e para o retângulo formado naquela região cada pixel tem seu valor modificado para 255 menos o valor do pixel, assim a cor fica em negativo.
1.3. Troca de Regiões
-
Utilizando o programa exemplos/pixels.cpp como referência, implemente um programa trocaregioes.cpp. Seu programa deverá trocar os quadrantes em diagonal na imagem. Explore o uso da classe Mat e seus construtores para criar as regiões que serão trocadas. O efeito é ilustrado na Figura Troca de regiões.
Para resolver essa questão implementamos o código abaixo:
#include <iostream>
#include <opencv2/opencv.hpp>
using namespace cv;
using namespace std;
int main(int, char**){
Mat image;//matriz de pixels da imagem
uchar aux;
int altura, largura;// altura e largura da imagem
image= imread("vanessa.png",CV_LOAD_IMAGE_GRAYSCALE);
if(!image.data)
cout << "nao abriu vanessa.png" << endl;
namedWindow("janela",WINDOW_AUTOSIZE);
altura=image.rows;
largura=image.cols;
//exibir a imagem antes de qualquer modificação
imshow("janela", image);
waitKey();
//para a primeira metade (i<altura/2 && j<largura/2) ||(i>=altura/2 && j>=largura/2)
//para inverter horizontalmente a coluna 0 recebe a col largura/2, a 1 recebe largura/2+1 e vice versa
//para inverter verticalmente a linha 0 recebe a lin altura/2, a 1 recebe altura/2+1 e vice versa
for(int i=0;i<altura/2;i++){
for(int j=0;j<largura/2;j++){
aux=image.at<uchar>(i,j);
image.at<uchar>(i,j)=image.at<uchar>(i+altura/2,j+largura/2);
image.at<uchar>(i+altura/2,j+largura/2)=aux;
}
}
//para a segunda metade (i<altura/2 && j>=largura/2) || (i>=altura/2 && j<largura/2)
//para inverter horizontalmente a coluna 0 recebe a col largura/2, a 1 recebe largura/2+1 e vice versa
//para inverter verticalmente a linha altura/2 recebe a lin 0, a altura/2+1 recebe a lin 1 e vice versa
for(int i=altura/2;i<altura;i++){
for(int j=0;j<largura/2;j++){
aux=image.at<uchar>(i,j);
image.at<uchar>(i,j)=image.at<uchar>(i-altura/2,j+largura/2);
image.at<uchar>(i-altura/2,j+largura/2)=aux;
}
}
//imagem invertida diagonalmente
imshow("janela", image);
waitKey();
return 0;
}
Utilizamos como imagem de entrada a mesma da tarefa anterior. Para trocar as regiões, entenderemos a imagem com 4 quadrantes:
Para trocar os quadrantes entre si identificamos 2 tipos de troca, quadrante 1 e 4 e outra situação entre o quadrante 2 e 3, vide o código mostrado acima.
As saídas do código foram:
1.4. Preenchendo regiões
1.5. Labeling em mais de 255 elementos
-
Observando-se o programa labeling.cpp como exemplo, é possível verificar que caso existam mais de 255 objetos na cena, o processo de rotulação poderá ficar comprometido. Identifique a situação em que isso ocorre e proponha uma solução para este problema.
O código labeling.png utiliza o número do objeto encontrado como o tom de preenchimento daquele objeto, de modo que no fim o objeto terá o valor máximo de nobjects. Desse modo,se houvessem mais de 255 objetos na cena, não poderíamos executar o floodFill utilizando o nobjects, pois trabalhamos com apenas 256 tons de cinza (de 0 a 255), bem como o fato de que o elemento 255 seria pintado com sua própria cor 255, o que é reduntante e produz um erro na contagem (já que um objeto normalmente não é formado por apenas um pixel, ele contaria o mesmo objeto várias vezes).
Uma solução para este problema seria passar a usar uma imagem colorida, com componentes RGB indo de 0 até 255, aumentariamos o valor máximo da contagem.
1.6. Contando elementos de uma Imagem
-
Aprimore o algoritmo de contagem apresentado para identificar regiões com ou sem buracos internos que existam na cena. Assuma que objetos com mais de um buraco podem existir. Inclua suporte no seu algoritmo para não contar bolhas que tocam as bordas da imagem. Não se pode presumir, a priori, que elas tenham buracos ou não.
Para resolver essa questão implementamos o código abaixo:
#include <iostream>
#include <opencv2/opencv.hpp>
using namespace cv;
using namespace std;
bool temBolha(Mat image,int i,int j){
int l,c;
CvPoint p;
Mat aux=image.clone(); //não queremos sobrescrescer a imagem principal
p.x=0;
p.y=0;
floodFill(aux,p,255); //pintamos o fundo de branco
while(aux.at<uchar>(i,j)!=255){
//expandir em 8 direções
//cima
l=i-1;
c=j;
while(aux.at<uchar>(l,c)!=255){
if(aux.at<uchar>(l,c)==0) //achamos uma região preta
return true;
l--;
}
//baixo
l=i+1;
c=j;
while(aux.at<uchar>(l,c)!=255){
if(aux.at<uchar>(l,c)==0) //achamos uma região preta
return true;
l++;
}
//esquerda
l=i;
c=j-1;
while(aux.at<uchar>(l,c)!=255){
if(aux.at<uchar>(l,c)==0) //achamos uma região preta
return true;
c--;
}
//direita
l=i;
c=j+1;
while(aux.at<uchar>(l,c)!=255){
if(aux.at<uchar>(l,c)==0) //achamos uma região preta
return true;
c++;
}
//lateral superior esquerda
l=i-1;
c=j-1;
while(aux.at<uchar>(l,c)!=255){
if(aux.at<uchar>(l,c)==0) //achamos uma região preta
return true;
l--;
c--;
}
//lateral superior direita
l=i-1;
c=j+1;
while(aux.at<uchar>(l,c)!=255){
if(aux.at<uchar>(l,c)==0) //achamos uma região preta
return true;
l--;
c++;
}
//lateral inferior esquerda
l=i+1;
c=j-1;
while(aux.at<uchar>(l,c)!=255){
if(aux.at<uchar>(l,c)==0) //achamos uma região preta
return true;
l++;
c--;
}
//lateral inferior direita
l=i+1;
c=j+1;
while(aux.at<uchar>(l,c)!=255){
if(aux.at<uchar>(l,c)==0) //achamos uma região preta
return true;
l++;
c++;
}
i++;
j++;
}
return false;
}
int main(int argc, char** argv){
Mat image;
int width, height;
int nobjects, nbolhas;
int i,j;
CvPoint p;
image = imread(argv[1],CV_LOAD_IMAGE_GRAYSCALE);
if(!image.data){
cout << "imagem nao carregou corretamente\n";
return(-1);
}
width=image.size().width;
height=image.size().height;
//despreza bolhas existentes na borda da imagem
p.x=0;
p.y=0;
// busca objetos com buracos presentes
for(i=0; i<height; i++){
if(image.at<uchar>(i,0) == 255){
p.x=0;
p.y=i;
floodFill(image,p,0);
}
if(image.at<uchar>(i,width-1) == 255){
p.x=width-1;
p.y=i;
floodFill(image,p,0);
}
}
for(j=0; j<width; j++){
if(image.at<uchar>(0,j) == 255){
p.x=j;
p.y=0;
floodFill(image,p,0);
}
if(image.at<uchar>(height-1,j) == 255){
p.x=j;
p.y=height-1;
floodFill(image,p,0);
}
}
// busca objetos com buracos presentes,lembrar de desconsiderar mais de 1 buraco
p.x=0;
p.y=0;
nobjects=0;
nbolhas=0;
for(i=0; i<height; i++){
for(j=0; j<width; j++){
if(image.at<uchar>(i,j) == 255){
// achou um objeto
nobjects++;
p.x=j;
p.y=i;
floodFill(image,p,nobjects);
//verificar se tem ao menos 1 bolha
if(temBolha(image,i,j)){
nbolhas++;
}
}
}
}//ao fim teremos nobjets presentes, bem como o número de objetos com ao menos 1 bolha
cout<<"Temos "<<nobjects<<", dos quais "<<nbolhas<<" possuem ao menos 1 bolha.\n";
//exibir e salvar os resultados
imshow("image", image);
imwrite("labeling.png", image);
waitKey();
return 0;
}
Esse programa é uma modificação do código labeling.cpp descrito nas atividades da disciplina. A mudança está no fato de que, para as regiões contabilizadas com o uso do floodfill, é feita uma análise para determinar se a região é ou não uma bolha.
Podemos perceber que esse código trata a situação de que uma bolha pode ter mais de 1 buraco, mas ainda ser a mesma bolha:
Percebe que o número de bolhas permanece 7, o que é o esperado.
1.7. Manipulação de histogramas
1.8. Equalização de histograma
-
Utilizando o programa exemplos/histogram.cpp como referência, implemente um programa equalize.cpp. Este deverá, para cada imagem capturada, realizar a equalização do histogram antes de exibir a imagem. Teste sua implementação apontando a câmera para ambientes com iluminações variadas e observando o efeito gerado. Assuma que as imagens processadas serão em tons de cinza.
Para resolver essa questão implementamos o código abaixo:
#include <iostream>
#include <opencv2/opencv.hpp>
using namespace cv;
using namespace std;
int main(int argc, char** argv){
Mat image;
int width, height;
VideoCapture cap;
vector<Mat> planes;
Mat histR, histG, histB;
int nbins = 64;
float range[] = {0, 256};
const float *histrange = { range };
bool uniform = true;
bool acummulate = false;
cap.open(0);
if(!cap.isOpened()){
cout << "cameras indisponiveis";
return -1;
}
width = cap.get(CV_CAP_PROP_FRAME_WIDTH);
height = cap.get(CV_CAP_PROP_FRAME_HEIGHT);
cout << "largura = " << width << endl;
cout << "altura = " << height << endl;
int histw = nbins, histh = nbins/2;
Mat histImgR(histh, histw, CV_8UC3, Scalar(0,0,0));
Mat histImgG(histh, histw, CV_8UC3, Scalar(0,0,0));
Mat histImgB(histh, histw, CV_8UC3, Scalar(0,0,0));
while(1){
cap >> image;
split (image, planes);
equalizeHist(planes[0],planes[0]);
equalizeHist(planes[1],planes[1]);
equalizeHist(planes[2],planes[2]);
merge(planes,image);
calcHist(&planes[0], 1, 0, Mat(), histR, 1,
&nbins, &histrange,
uniform, acummulate);
calcHist(&planes[1], 1, 0, Mat(), histG, 1,
&nbins, &histrange,
uniform, acummulate);
calcHist(&planes[2], 1, 0, Mat(), histB, 1,
&nbins, &histrange,
uniform, acummulate);
normalize(histR, histR, 0, histImgR.rows, NORM_MINMAX, -1, Mat());
normalize(histG, histG, 0, histImgG.rows, NORM_MINMAX, -1, Mat());
normalize(histB, histB, 0, histImgB.rows, NORM_MINMAX, -1, Mat());
histImgR.setTo(Scalar(0));
histImgG.setTo(Scalar(0));
histImgB.setTo(Scalar(0));
for(int i=0; i<nbins; i++){
line(histImgR,
Point(i, histh),
Point(i, histh-cvRound(histR.at<float>(i))),
Scalar(0, 0, 255), 1, 8, 0);
line(histImgG,
Point(i, histh),
Point(i, histh-cvRound(histG.at<float>(i))),
Scalar(0, 255, 0), 1, 8, 0);
line(histImgB,
Point(i, histh),
Point(i, histh-cvRound(histB.at<float>(i))),
Scalar(255, 0, 0), 1, 8, 0);
}
histImgR.copyTo(image(Rect(0, 0 ,nbins, histh)));
histImgG.copyTo(image(Rect(0, histh ,nbins, histh)));
histImgB.copyTo(image(Rect(0, 2*histh ,nbins, histh)));
imshow("image", image);
if(waitKey(30) >= 0)
break;
}
return 0;
}
Esse programa é uma modificação do código histogram.cpp descrito nas atividades da disciplina. A mudança está no fato de que, para cada histograma foi feita uma equalização.
Foi feito também um vídeo comparativo:
-
video não equalizado https://youtu.be/gEhkJSrBFoQ
-
video equalizado https://youtu.be/iXpXGFOPxrU
1.9. Detector de Movimento
-
Utilizando o programa exemplos/histogram.cpp como referência, implemente um programa motiondetector.cpp. Este deverá continuamente calcular o histograma da imagem (apenas uma componente de cor é suficiente) e compará-lo com o último histograma calculado. Quando a diferença entre estes ultrapassar um limiar pré-estabelecido, ative um alarme. Utilize uma função de comparação que julgar conveniente.
Para resolver essa questão implementamos o código abaixo:
#include <iostream>
#include <opencv2/opencv.hpp>
using namespace cv;
using namespace std;
int main(int argc, char** argv){
Mat image;
int width, height;
VideoCapture cap;
vector<Mat> planes;
Mat histR,histOldR;
int nbins = 64;
float range[] = {0, 256};
const float *histrange = { range };
bool uniform = true;
bool acummulate = false;
cap.open(0);
if(!cap.isOpened()){
cout << "cameras indisponiveis";
return -1;
}
width = cap.get(CV_CAP_PROP_FRAME_WIDTH);
height = cap.get(CV_CAP_PROP_FRAME_HEIGHT);
cout << "largura = " << width << endl;
cout << "altura = " << height << endl;
int histw = nbins, histh = nbins/2;
Mat histImgR(histh, histw, CV_8UC3, Scalar(0,0,0));
cap >> image;
split (image, planes);
calcHist(&planes[0], 1, 0, Mat(), histR, 1,
&nbins, &histrange,
uniform, acummulate);
normalize(histR, histR, 0, histImgR.rows, NORM_MINMAX, -1, Mat());
histImgR.setTo(Scalar(0));
for(int i=0; i<nbins; i++){
line(histImgR,
Point(i, histh),
Point(i, histh-cvRound(histR.at<float>(i))),
Scalar(0, 0, 255), 1, 8, 0);
}
histImgR.copyTo(image(Rect(0, 0 ,nbins, histh)));
imshow("image", image);
while(1){
cap >> image;
split (image, planes);
histOldR=histR.clone();
calcHist(&planes[0], 1, 0, Mat(), histR, 1,
&nbins, &histrange,
uniform, acummulate);
normalize(histR, histR, 0, histImgR.rows, NORM_MINMAX, -1, Mat());
histImgR.setTo(Scalar(0));
for(int i=0; i<nbins; i++){
line(histImgR,
Point(i, histh),
Point(i, histh-cvRound(histR.at<float>(i))),
Scalar(0, 0, 255), 1, 8, 0);
}
histImgR.copyTo(image(Rect(0, 0 ,nbins, histh)));
double aux = compareHist(histR,histOldR,CV_COMP_CORREL);
if(aux<=0.99){
putText(image, "Motion Detected", cvPoint(image.cols/2 - 50, 40), FONT_HERSHEY_COMPLEX_SMALL, 1, Scalar(0, 0, 255), 2);
}
imshow("image", image);
if(waitKey(30) >= 0) break;
}
return 0;
}
Esse programa é uma modificação do código histogram.cpp descrito nas atividades da disciplina. Ao invés de calcularmos os histogramas RGB, calculamos apenas o histR. Feito isso, criamos uma variável do tipo Mat que recebe o histR anterior. Por fim, comparamos esses 2 histogramas pela função aux=compareHist(histR,histOldR,CV_COMP_CORREL) e se aux<=0.99 (valor arbitrário) disparamos um "alarme". Veja o vídeo mostrando o funcionamento do código:
-
video do detector de movimento https://youtu.be/hDpLISPe9zc
1.10. Filtragem no domínio espacial I
===Laplaciano do Gaussiano
-
Utilizando o programa exemplos/filtroespacial.cpp como referência, implemente um programa laplgauss.cpp. O programa deverá acrescentar mais uma funcionalidade ao exemplo fornecido, permitindo que seja calculado o laplaciano do gaussiano das imagens capturadas. Compare o resultado desse filtro com a simples aplicação do filtro laplaciano.
Para resolver essa questão implementamos o código abaixo:
#include <iostream>
#include <opencv2/opencv.hpp>
using namespace cv;
using namespace std;
void printmask(Mat &m){
for(int i=0; i<m.size().height; i++){
for(int j=0; j<m.size().width; j++){
cout << m.at<float>(i,j) << ",";
}
cout << endl;
}
}
void menu(){
cout << "\npressione a tecla para ativar o filtro: \n"
"a - calcular modulo\n"
"m - media\n"
"g - gauss\n"
"v - vertical\n"
"h - horizontal\n"
"l - laplaciano\n"
"z - laplaciano do gaussiano\n"
"esc - sair\n";
}
int main(int argvc, char** argv){
VideoCapture video;
float media[] = {1,1,1,
1,1,1,
1,1,1};
float gauss[] = {1,2,1,
2,4,2,
1,2,1};
float horizontal[]={-1,0,1,
-2,0,2,
-1,0,1};
float vertical[]={-1,-2,-1,
0,0,0,
1,2,1};
float laplacian[]={0,-1,0,
-1,4,-1,
0,-1,0};
float laplacianGauss[]= {0,0,1,0,0,
0,1,2,1,0,
1,2,-16,2,1,
0,1,2,1,0,
0,0,1,0,0};
Mat cap, frame, frame32f, frameFiltered;
Mat mask(3,3,CV_32F), mask1;
Mat result, result1;
double width, height, min, max;
int absolut;
char key;
video.open(0);
if(!video.isOpened())
return -1;
width=video.get(CV_CAP_PROP_FRAME_WIDTH);
height=video.get(CV_CAP_PROP_FRAME_HEIGHT);
std::cout << "largura=" << width << "\n";;
std::cout << "altura =" << height<< "\n";;
namedWindow("filtroespacial",1);
mask = Mat(3, 3, CV_32F, media);
scaleAdd(mask, 1/9.0, Mat::zeros(3,3,CV_32F), mask1);
swap(mask, mask1);
absolut=1; // calcs abs of the image
menu();
while(1){
video >> cap;
cvtColor(cap, frame, CV_BGR2GRAY);
flip(frame, frame, 1);
imshow("original", frame);
frame.convertTo(frame32f, CV_32F);
filter2D(frame32f, frameFiltered, frame32f.depth(), mask, Point(1,1), 0);
if(absolut){
frameFiltered=abs(frameFiltered);
}
frameFiltered.convertTo(result, CV_8U);
imshow("filtroespacial", result);
key = (char) waitKey(10);
if( key == 27 ) break; // esc pressed!
switch(key){
case 'a':
menu();
absolut=!absolut;
break;
case 'm':
menu();
mask = Mat(3, 3, CV_32F, media);
scaleAdd(mask, 1/9.0, Mat::zeros(3,3,CV_32F), mask1);
mask = mask1;
printmask(mask);
break;
case 'g':
menu();
mask = Mat(3, 3, CV_32F, gauss);
scaleAdd(mask, 1/16.0, Mat::zeros(3,3,CV_32F), mask1);
mask = mask1;
printmask(mask);
break;
case 'h':
menu();
mask = Mat(3, 3, CV_32F, horizontal);
printmask(mask);
break;
case 'v':
menu();
mask = Mat(3, 3, CV_32F, vertical);
printmask(mask);
break;
case 'l':
menu();
mask = Mat(3, 3, CV_32F, laplacian);
printmask(mask);
break;
case 'z':
menu();
mask = Mat(5, 5, CV_32F, laplacianGauss);
printmask(mask);
break;
default:
break;
}
}
return 0;
}
Esse programa é uma modificação do código filtroespacial.cpp descrito nas atividades da disciplina. Foi feita um filtro chamado laplacianGauss e esse filtro corresponde ao filtro laplaciano do gaussiano.
Comparando um com o outro, notamos que no laplaciano do gaussiano as bordas aparecem mais evidenciadas. No entanto, há aumento de ruído em relação à imagem apenas do laplaciano.
-
video de demonstração https://youtu.be/Fn79ATVbtjU
1.11. Filtragem no domínio espacial II
1.12. Tiltshift
-
Utilizando o programa exemplos/addweighted.cpp como referência, implemente um programa tiltshift.cpp. Três ajustes deverão ser providos na tela da interface:
-
um ajuste para regular a altura da região central que entrará em foco;
-
um ajuste para regular a força de decaimento da região borrada; *um ajuste para regular a posição vertical do centro da região que entrará em foco. Finalizado o programa, a imagem produzida deverá ser salva em arquivo.
Para resolver essa questão implementamos o código abaixo:
#include <iostream>
#include <opencv2/opencv.hpp>
using namespace cv;
using namespace std;
double d=0;
int l1=0,l2=0,h=0;
int width,height;
int centro=0;
int d_slider = 0;
int d_slider_max = 100;
int center_slider = 0;
int center_slider_max = 100;
int height_slider = 0;
int height_slider_max = 100;
Mat image;
Mat image_blurred;
Mat ganhoImage;
Mat ganhoImage_blurred;
Mat imageTiltShift;
char TrackbarName[50];
void makeBlurredImage(){
Mat result, result1;
int absolut=1;
image.copyTo(result);
float media[] = {1,1,1,1,1,1,1,1,1,
1,1,1,1,1,1,1,1,1,
1,1,1,1,1,1,1,1,1,
1,1,1,1,1,1,1,1,1,
1,1,1,1,1,1,1,1,1,
1,1,1,1,1,1,1,1,1,
1,1,1,1,1,1,1,1,1,
1,1,1,1,1,1,1,1,1,
1,1,1,1,1,1,1,1,1};
Mat mask(9,9,CV_32F,media),mask1;
mask = Mat(9, 9, CV_32F, media);
scaleAdd(mask, 1/81.0, Mat::zeros(9,9,CV_32F), mask1);
mask = mask1;
result.convertTo(result1, CV_32F);
filter2D(result1, image_blurred, result1.depth(), mask, Point(1,1), 0);
if(absolut){
image_blurred=abs(image_blurred);
}
image_blurred.convertTo(image_blurred, CV_8U);
}
void blend(){
int i,j;
image.copyTo(ganhoImage);
image.copyTo(ganhoImage_blurred);
image.copyTo(imageTiltShift);
//ganho da imagem de entrada
for(i=0; i<height; i++){
for(j=0; j<width; j++){
ganhoImage.at<uchar>(i,j)= 255*(1/2.0)*(tanh((i-l1)/(d*1.0))-tanh((i-l2)/(d*1.0)));
}
}
//ganho da imagem borrada
for(i=0; i<height; i++){
for(j=0; j<width; j++){
ganhoImage_blurred.at<uchar>(i,j)= 255- ganhoImage.at<uchar>(i,j);
}
}
//gerar imagem final como combinação
for(i=0; i<height; i++){
for(j=0; j<width; j++){
imageTiltShift.at<uchar>(i,j)= (ganhoImage.at<uchar>(i,j)/255.0)*image.at<uchar>(i,j)+(ganhoImage_blurred.at<uchar>(i,j)/255.0)*image_blurred.at<uchar>(i,j);
}
}
imshow("tiltshift",imageTiltShift);
}
void on_trackbar_height(int, void*){
h = height_slider*height/100;
l1 = centro - h/2;
l2 = centro + h/2;
if(l1<0){
l1=0;
}
if(l2>height){
l2=height;
}
blend();
}
void on_trackbar_center(int, void*){
centro=center_slider*height/100;
l1 = centro - h/2;
l2 = centro + h/2;
if(l1<0){
l1=0;
}
if(l2>height){
l2=height;
}
blend();
}
void on_trackbar_decaimento(int, void*){
//gerar as imagens para combinação a posterior das duas imagens
d= d_slider;
blend();
}
int main(int argvc, char** argv){
image = imread(argv[1],CV_LOAD_IMAGE_GRAYSCALE);
if(!image.data){
cout << "imagem nao carregou corretamente\n";
return(-1);
}
//largura e altura da imagem
width=image.size().width;
height=image.size().height;
//criar a imagem borrada
makeBlurredImage();
namedWindow("tiltshift", 1);
//setar a altura
sprintf( TrackbarName, "Altura: " );
createTrackbar( TrackbarName, "tiltshift",
&height_slider,
height_slider_max,
on_trackbar_height );
on_trackbar_height(height_slider, 0 );
//definir cetro do foco e calcular l1 e l2
sprintf( TrackbarName, "Centro: ");
createTrackbar( TrackbarName, "tiltshift",
¢er_slider,
center_slider_max,
on_trackbar_center );
on_trackbar_center(center_slider, 0 );
//calcula decaimento e junta as 2 imagens
sprintf( TrackbarName, "Decaimento: " );
createTrackbar( TrackbarName, "tiltshift",
&d_slider,
d_slider_max,
on_trackbar_decaimento );
on_trackbar_decaimento(d_slider, 0 );
waitKey(0);
return 0;
}
Esse programa é uma modificação do código addweighted.cpp descrito nas atividades da disciplina. Foi gerado o efeito de tiltshift através da combinação de duas imagens, a imagem de entrada e a imagem borrada.
-
video para exemplificação
1.13. Tiltshift Video
-
Utilizando o programa exemplos/addweighted.cpp como referência, implemente um programa tiltshiftvideo.cpp. Tal programa deverá ser capaz de processar um arquivo de vídeo, produzir o efeito de tilt-shift nos quadros presentes e escrever o resultado em outro arquivo de vídeo. A ideia é criar um efeito de miniaturização de cenas. Descarte quadros em uma taxa que julgar conveniente para evidenciar o efeito de stop motion, comum em vídeos desse tipo.
Para resolver essa questão implementamos o código abaixo:
#include <iostream>
#include <opencv2/opencv.hpp>
using namespace cv;
using namespace std;
double d=0;
int l1=0,l2=0,h=0;
int width,height;
int centro=0;
int d_slider = 0;
int d_slider_max = 100;
int center_slider = 0;
int center_slider_max = 100;
int height_slider = 0;
int height_slider_max = 100;
VideoCapture cap;
Mat image;
Mat image_blurred;
Mat ganhoImage;
Mat ganhoImage_blurred;
Mat imageTiltShift;
char TrackbarName[50];
void makeBlurredImage(){
Mat result, result1;
int absolut=1;
image.copyTo(result);
float media[] = {1,1,1,1,1,1,1,1,1,
1,1,1,1,1,1,1,1,1,
1,1,1,1,1,1,1,1,1,
1,1,1,1,1,1,1,1,1,
1,1,1,1,1,1,1,1,1,
1,1,1,1,1,1,1,1,1,
1,1,1,1,1,1,1,1,1,
1,1,1,1,1,1,1,1,1,
1,1,1,1,1,1,1,1,1};
Mat mask(9,9,CV_32F,media),mask1;
mask = Mat(9, 9, CV_32F, media);
scaleAdd(mask, 1/81.0, Mat::zeros(9,9,CV_32F), mask1);
mask = mask1;
result.convertTo(result1, CV_32F);
filter2D(result1, image_blurred, result1.depth(), mask, Point(1,1), 0);
if(absolut){
image_blurred=abs(image_blurred);
}
image_blurred.convertTo(image_blurred, CV_8U);
}
void blend(){
int i,j;
image.copyTo(ganhoImage);
image.copyTo(ganhoImage_blurred);
image.copyTo(imageTiltShift);
//ganho da imagem de entrada
for(i=0; i<height; i++){
for(j=0; j<width; j++){
ganhoImage.at<uchar>(i,j)= 255*(1/2.0)*(tanh((i-l1)/(d*1.0))-tanh((i-l2)/(d*1.0)));
}
}
//ganho da imagem borrada
for(i=0; i<height; i++){
for(j=0; j<width; j++){
ganhoImage_blurred.at<uchar>(i,j)= 255- ganhoImage.at<uchar>(i,j);
}
}
//gerar imagem final como combinação
for(i=0; i<height; i++){
for(j=0; j<width; j++){
imageTiltShift.at<uchar>(i,j)= (ganhoImage.at<uchar>(i,j)/255.0)*image.at<uchar>(i,j)+(ganhoImage_blurred.at<uchar>(i,j)/255.0)*image_blurred.at<uchar>(i,j);
}
}
}
void on_trackbar_height(int, void*){
h = height_slider*height/100;
l1 = centro - h/2;
l2 = centro + h/2;
if(l1<0){
l1=0;
}
if(l2>height){
l2=height;
}
}
void on_trackbar_center(int, void*){
centro=center_slider*height/100;
l1 = centro - h/2;
l2 = centro + h/2;
if(l1<0){
l1=0;
}
if(l2>height){
l2=height;
}
}
void on_trackbar_decaimento(int, void*){
//gerar as imagens para combinação a posterior das duas imagens
d= d_slider;
}
int main(int argvc, char** argv){
cap.open(0);
if(!cap.isOpened())
return -1;
width=cap.get(CV_CAP_PROP_FRAME_WIDTH);
height=cap.get(CV_CAP_PROP_FRAME_HEIGHT);
namedWindow("tiltshift", 1);
while(1){
cap >> image;
cvtColor(image, image, CV_BGR2GRAY);
//criar a imagem borrada
makeBlurredImage();
//setar a altura
sprintf( TrackbarName, "Altura: " );
createTrackbar( TrackbarName, "tiltshift",
&height_slider,
height_slider_max,
on_trackbar_height );
on_trackbar_height(height_slider, 0 );
//definir cetro do foco e calcular l1 e l2
sprintf( TrackbarName, "Centro: ");
createTrackbar( TrackbarName, "tiltshift",
¢er_slider,
center_slider_max,
on_trackbar_center );
on_trackbar_center(center_slider, 0 );
//calcula decaimento e junta as 2 imagens
sprintf( TrackbarName, "Decaimento: " );
createTrackbar( TrackbarName, "tiltshift",
&d_slider,
d_slider_max,
on_trackbar_decaimento );
on_trackbar_decaimento(d_slider, 0 );
blend();
imshow("tiltshift",imageTiltShift);
waitKey(1);
}
//waitKey(0);
return 0;
}
Esse programa é uma modificação do código tiltshift.cpp descrito no exemplo anterior. Foi gerado o efeito de tiltshift para um video.
-
video para exemplificação https://youtu.be/da_k0FoD-_s
2. Unidade 2
2.1. Filtragem no Domínio da Frequência
2.2. Filtro Homomórfico
-
Utilizando o programa exemplos/dft.cpp como referência, implemente o filtro homomórfico para melhorar imagens com iluminação irregular. Crie uma cena mal iluminada e ajuste os parâmetros do filtro homomórfico para corrigir a iluminação da melhor forma possível. Assuma que a imagem fornecida é em tons de cinza.
Para resolver essa questão implementamos o código abaixo:
#include <iostream>
#include <math.h>
#include <opencv2/opencv.hpp>
using namespace cv;
using namespace std;
const int MAX=10;
int dft_M, dft_N;
double C,D0,gammaH,gammaL;
int highFrequency_slider = 20;
int highFrequency_slider_max = 50;
int lowFrequency_slider = 2;
int lowFrequency_slider_max = 10;
int smoothness_slider = 1;
int smoothness_slider_max = 100;
int cutoff_slider = 5;
int cutoff_slider_max = 200;
Mat complexImage, tmp,imageFiltered;
Mat padded, filter;
Mat image;
Mat_<float> realInput, zeros;
vector<Mat> planos;
char TrackbarName[50];
// troca os quadrantes da imagem da DFT
void deslocaDFT(Mat& image ){
Mat aux, A, B, C, D;
// se a imagem tiver tamanho impar, recorta a regiao para
// evitar cópias de tamanho desigual
image = image(Rect(0, 0, image.cols & -2, image.rows & -2));
int cx = image.cols/2;
int cy = image.rows/2;
// reorganiza os quadrantes da transformada
// A B -> D C
// C D B A
A = image(Rect(0, 0, cx, cy));
B = image(Rect(cx, 0, cx, cy));
C = image(Rect(0, cy, cx, cy));
D = image(Rect(cx, cy, cx, cy));
// A <-> D
A.copyTo(aux); D.copyTo(A); aux.copyTo(D);
// C <-> B
C.copyTo(aux); B.copyTo(C); aux.copyTo(B);
}
void homomorphicFiltering(){
// a função de transferência (filtro frequencial) deve ter o
// mesmo tamanho e tipo da matriz complexa
filter = Mat(padded.size(), CV_32FC2, Scalar(0));
// cria uma matriz temporária para criar as componentes real
// e imaginaria do filtro ideal
tmp = Mat(dft_M, dft_N, CV_32F);
// prepara o filtro passa-baixas ideal
for(int i=0; i<dft_M; i++){
for(int j=0; j<dft_N; j++){
tmp.at<float>(i,j)= (gammaH-gammaL)*(1-expf((-1.0*C*(pow(i-dft_M/2.0,2)+pow(j-dft_N/2.0,2)))/(pow(D0,2))))+gammaL;
}
}
// cria a matriz com as componentes do filtro e junta
// ambas em uma matriz multicanal complexa
Mat comps[]= {tmp, tmp};
merge(comps, 2, filter);
}
void applyFilter(void){
//limpa os planos
planos.clear();
// parte imaginaria da matriz complexa (preenchida com zeros)
zeros = Mat_<float>::zeros(padded.size());
// prepara a matriz complexa para ser preenchida
complexImage = Mat(padded.size(), CV_32FC2, Scalar(0));
// cria a compoente real
realInput = Mat_<float>(padded);
//realInput += Scalar::all(1);
//log(realInput,realInput);
// insere as duas componentes no array de matrizes
planos.push_back(realInput);
planos.push_back(zeros);
// combina o array de matrizes em uma unica
// componente complexa
merge(planos, complexImage);
// calcula o dft
dft(complexImage, complexImage);
// realiza a troca de quadrantes
deslocaDFT(complexImage);
//redimensiona e normaliza para remoção de bordas
resize(complexImage,complexImage,padded.size());
normalize(complexImage,complexImage,0,1,CV_MINMAX);
//calcula o filtro homomorfico
homomorphicFiltering();
// aplica o filtro frequencial
mulSpectrums(complexImage,filter,complexImage,0);
// troca novamente os quadrantes
deslocaDFT(complexImage);
// calcula a DFT inversa
idft(complexImage, complexImage);
// limpa o array de planos
planos.clear();
// separa as partes real e imaginaria da
// imagem filtrada
split(complexImage, planos);
//exp(planos[0],planos[0]);
// normaliza a parte real para exibicao
normalize(planos[0], planos[0], 0, 1, CV_MINMAX);
//imagem filtrada recebe o planos[0]
imageFiltered = planos[0].clone();
printf("D0: %f\n", D0);
printf("C: %f\n", C);
printf("Alta Frequencia: %f\n", gammaH);
printf("Baixa Frequencia: %f\n", gammaL);
imshow("Homomorphic filtering", imageFiltered);
imshow("Original", image);
}
void on_trackbar_highFrequency(int, void*){
gammaH = highFrequency_slider/MAX;
applyFilter();
}
void on_trackbar_lowFrequency(int, void*){
gammaL = lowFrequency_slider/MAX;
applyFilter();
}
void on_trackbar_smoothness(int, void*){
C = smoothness_slider;
applyFilter();
}
void on_trackbar_cutoff(int, void*){
D0 = cutoff_slider;
applyFilter();
}
int main(int argvc, char** argv){
//carrega a imagem passada como argumento
image = imread(argv[1],CV_LOAD_IMAGE_GRAYSCALE);
if(!image.data){
cout << "imagem nao carregou corretamente\n";
return(-1);
}
// identifica os tamanhos otimos para
// calculo do FFT
dft_M = getOptimalDFTSize(image.rows);
dft_N = getOptimalDFTSize(image.cols);
// realiza o padding da imagem
copyMakeBorder(image, padded, 0,
dft_M - image.rows, 0,
dft_N - image.cols,
BORDER_CONSTANT, Scalar::all(0));
//prepara a janela para exibição da imagem
namedWindow("Homomorphic filtering", 1);
//setar HighFrequency (reflectância)
sprintf( TrackbarName, "GammaH->High Frequency: " );
createTrackbar( TrackbarName, "Homomorphic filtering",
&highFrequency_slider,
highFrequency_slider_max,
on_trackbar_highFrequency );
on_trackbar_highFrequency(highFrequency_slider, 0 );
//setar LowFrequency (iluminância)
sprintf( TrackbarName, "GammaL->Low Frequency: " );
createTrackbar( TrackbarName, "Homomorphic filtering",
&lowFrequency_slider,
lowFrequency_slider_max,
on_trackbar_lowFrequency );
on_trackbar_lowFrequency(lowFrequency_slider, 0 );
//setar smoothness (Suavidade da curva do filtro)
sprintf( TrackbarName, "C->Smoothness of the curve: " );
createTrackbar( TrackbarName, "Homomorphic filtering",
&smoothness_slider,
smoothness_slider_max,
on_trackbar_smoothness );
on_trackbar_smoothness(smoothness_slider, 0 );
//setar cutoff (D0-> frequência de corte -> raio do filtro)
sprintf( TrackbarName, "D0->Cutoff Frequency: " );
createTrackbar( TrackbarName, "Homomorphic filtering",
&cutoff_slider,
cutoff_slider_max,
on_trackbar_cutoff );
on_trackbar_cutoff(cutoff_slider, 0 );
applyFilter();;
waitKey(0);
return 0;
}
Esse programa é uma modificação do código dft.cpp descrito nas atividades da disciplina, um filtro homomórfico consiste em:
Foi gerado o efeito pedido com ajuste dos parâmetros C, D0, gammaL e gammaH. Sendo o C responsável pela suavidade do filtro, o D0 o raio do filtro, o gammaL corresponde à iluminância(baixas frequências) e o gammaH à reflectância(altas frequências).
A entrada do programa foi:
A saída do programa foi:
Para exemplificação foi gravado um vídeo:
-
video para exemplificação https://youtu.be/zyedRpG-4LI
2.3. Detecção de bordas com o algoritmo de Canny
2.4. Refinamento da Técnica do Pontilhismo
-
Utilizando os programas exemplos/canny.cpp e exemplos/pontilhismo.cpp como referência, implemente um programa cannypoints.cpp. A idéia é usar as bordas produzidas pelo algoritmo de Canny para melhorar a qualidade da imagem pontilhista gerada. A forma como a informação de borda será usada é livre. Entretanto, são apresentadas algumas sugestões de técnicas que poderiam ser utilizadas:
-
Desenhar pontos grandes na imagem pontilhista básica;
-
Usar a posição dos pixels de borda encontrados pelo algoritmo de Canny para desenhar pontos nos respectivos locais na imagem gerada.
-
Experimente ir aumentando os limiares do algoritmo de Canny e, para cada novo par de limiares, desenhar círculos cada vez menores nas posições encontradas. A Figura Pontilhismo aplicado à imagem Lena foi desenvolvida usando essa técnica.
-
Escolha uma imagem de seu gosto e aplique a técnica que você desenvolveu.
-
Descreva no seu relatório detalhes do procedimento usado para criar sua técnica pontilhista.
Para resolver essa questão implementamos o código abaixo:
#include <iostream>
#include <opencv2/opencv.hpp>
#include <fstream>
#include <iomanip>
#include <vector>
#include <algorithm>
#include <numeric>
#include <ctime>
#include <cstdlib>
#define R_inicial 5
#define JITTER 3
#define STEP 10
using namespace std;
using namespace cv;
//esses valores serão modificados conforme aumentarmos os limiares T1 e T2
double RAIO=R_inicial;
Mat image, points;
int width, height;
void pontilhismo(){
int x,y,gray;
vector<int> yrange;
vector<int> xrange;
xrange.resize(height/STEP);
yrange.resize(width/STEP);
iota(xrange.begin(), xrange.end(), 0);
iota(yrange.begin(), yrange.end(), 0);
for(uint i=0; i<xrange.size(); i++){
xrange[i]= xrange[i]*STEP+STEP/2;
}
for(uint i=0; i<yrange.size(); i++){
yrange[i]= yrange[i]*STEP+STEP/2;
}
points = Mat(height, width, CV_8U, Scalar(255));
random_shuffle(xrange.begin(), xrange.end());
for(auto i : xrange){
random_shuffle(yrange.begin(), yrange.end());
for(auto j : yrange){
x = i+rand()%(2*JITTER)-JITTER+1;
y = j+rand()%(2*JITTER)-JITTER+1;
gray = image.at<uchar>(x,y);
circle(points,
cv::Point(y,x),
RAIO,
CV_RGB(gray,gray,gray),
-1,
CV_AA);
}
}
}
void canny(){
int x,y,gray;
Mat border;
int i,nIteracoes=10;
int t2=1;
for(i=0;i<nIteracoes;i++){
Canny(image, border, t2, 3*t2);
t2+=10;
//refinar pointilhismo
//para os pontos da borda desenhar circulos menores
for(x=0;x<height;x++){
for(y=0;y<width;y++){
if(border.at<uchar>(x,y)==255){
gray = image.at<uchar>(x,y);
circle(points,cv::Point(y,x),RAIO,CV_RGB(gray,gray,gray),-1,CV_AA);
}
}
}
//diminuir o raio para ponto menor
RAIO=int(RAIO-R_inicial/(1.0*nIteracoes));
}
}
int main(int argc, char** argv){
Mat copiaOriginal,copiaRefinada;
// guarda tecla capturada
char key;
//determinar se quer refinar pontilhismo ou nao
bool troca=1;
//carregar imagem
image= imread(argv[1],CV_LOAD_IMAGE_GRAYSCALE);
srand(time(0));
if(!image.data){
cout << "nao abriu" << argv[1] << endl;
cout << argv[0] << " imagem.jpg";
exit(0);
}
//altura e largura da imagem
width=image.size().width;
height=image.size().height;
//prepara a janela para exibição da imagem
namedWindow("Pointilhismo", 1);
//construção da imagem pointilhista básica
pontilhismo();
//guarda copia
points.copyTo(copiaOriginal);
//utilizar canny para refinar algoritmo
canny();
points.copyTo(copiaRefinada);
while(1){
key = (char) waitKey(10);
if( key == 27 ) break; // esc pressed!
if(key=='t'){
if(troca)
copiaRefinada.copyTo(points);
else
copiaOriginal.copyTo(points);
troca=!troca;
}
//resultados
imshow("Pointilhismo", points);
}
imwrite("pontos.jpg", points);
return 0;
}
Esse programa é uma modificação do código canny.cpp e pontilhismo.cpp descrito nas atividades da disciplina, pontilhismo consiste em uma técnica de pintura e desenho em que as imagens são definidas por pequenas manchas ou pontos.
Foi gerado o efeito pedido através do refinamento dos pontos a serem desenhados. Para tanto, primeiro foi executado o trecho de código do pontilhismo básico (descrito no tutorial) e, a posteriori, é utilizado o algoritmo de canny em um loop com 10 iterações. Para cada iteração alteramos o raio do circulo a ser desenhado (diminuindo-o) e os limiares T1 e T2 que estabelecem a relação de pontos de bordas fortes e fracas da imagem de bordas. Então, percorremos a imagem de bordas e para os pontos que fazem parte da mesma (em branco), desenhamos o círculo com o raio estabelecido para aquela iteração. Além disso apertando a tecla 't' é possível alternar do pontilhismo refinado para o simples.
A entrada do programa foi:
A saída do programa foi:
-
Para o código pontilhista simples:
-
Para o código pontilhista Refinado:
Além disso, para diversão, foi feito uso de outras imagens, cujas saídas refinadas estão mostradas abaixo:
Para exemplificação foi gravado um vídeo:
-
video para exemplificação. Apertando a tecla 't' podemos comparar os efeitos https://youtu.be/sNdNbY2kX48
2.5. Quantização vetorial com k-means
2.6. K-means com 10 iterações, nRodadas=1 e centros com inicialização aleatória
-
Utilizando o programa kmeans.cpp como exemplo prepare um programa exemplo onde a execução do código se dê usando o parâmetro nRodadas=1 e inciar os centros de forma aleatória usando o parâmetro KMEANS_RANDOM_CENTERS ao invés de KMEANS_PP_CENTERS. Realize 10 rodadas diferentes do algoritmo e compare as imagens produzidas. Explique porque elas podem diferir tanto.
Para resolver essa questão implementamos o código abaixo:
#include <opencv2/opencv.hpp>
#include <cstdlib>
#include <cstring>
using namespace std;
using namespace cv;
int main( int argc, char** argv ){
int ASCII_NUMBERS ='0';
int nClusters = 15;
Mat rotulos;
int nRodadas = 1; //conforme pedido na questão
Mat centros;
if(argc!=3){
exit(0);
}
Mat img = imread( argv[1], CV_LOAD_IMAGE_COLOR);
//mostrar imagem original
imshow( "clustered image", img );
waitKey( 1 );
for(int i = 0; i < 10; i++){
Mat samples(img.rows * img.cols, 3, CV_32F);
for( int y = 0; y < img.rows; y++ ){
for( int x = 0; x < img.cols; x++ ){
for( int z = 0; z < 3; z++){
samples.at<float>(y + x*img.rows, z) = img.at<Vec3b>(y,x)[z];
}
}
}
kmeans(samples,
nClusters,
rotulos,
TermCriteria(CV_TERMCRIT_ITER|CV_TERMCRIT_EPS, 10000, 0.0001),
nRodadas,
KMEANS_RANDOM_CENTERS,//iniciando centros de forma aleatória
centros );
Mat rotulada( img.size(), img.type() );
for( int y = 0; y < img.rows; y++ ){
for( int x = 0; x < img.cols; x++ ){
int indice = rotulos.at<int>(y + x*img.rows,0);
rotulada.at<Vec3b>(y,x)[0] = (uchar) centros.at<float>(indice, 0);
rotulada.at<Vec3b>(y,x)[1] = (uchar) centros.at<float>(indice, 1);
rotulada.at<Vec3b>(y,x)[2] = (uchar) centros.at<float>(indice, 2);
}
}
imshow( "clustered image", rotulada );
char nome[50]="Saida";
nome[ strlen(nome) ] = (char)(i + ASCII_NUMBERS);
nome[ strlen(nome) + 1] = 0x0;
strcat(nome,argv[1]);
imwrite(nome, rotulada);
waitKey( 1 );
}
}
O algoritmo de K-means aprensentado consiste em uma segmentação de cores. Logo, vejamos um pseudocódigo do algorítmo de K-means:
Esse programa é uma modificação do código kmeans.cpp descrito nas atividades da disciplina. Para o programa, utilizamos um loop com 10 iterações, em cada iteração os centros do algoritmo de kmeans eram inicializados aleatoriamente, diferentemente para cada loop.
As entradas do programa foram:
A saída do programa pode ser vista no video para exemplificação:
-
video para exemplificação https://youtu.be/gD3hcCqJxkw
Como podemos perceber a imagem gerada difere para cada inicialização de centros. O algoritmo escolhe centros que irão definir quais cores serão utilizadas para representar a imagem final (no nosso algoritmo definimos 15 cores). Desse modo, gerando os centros de maneira aleatória, o resultado final para cada centro é diferente, por isso a imagem varia a cada iteração.
3. Unidade 3
3.1. Trabalho Final
Acessar via Trabalho Final